/**
 * 
 */
package com.star.exception.handler;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.util.CollectionUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver;
import org.springframework.web.util.WebUtils;

import com.star.exception.converter.RestErrorConverter;
import com.star.exception.converter.impl.MapRestErrorConverter;
import com.star.exception.converter.resolver.RestErrorResolver;
import com.star.exception.converter.resolver.impl.DefaultRestErrorResolver;
import com.star.exception.entity.RestError;

/**
 * @ClassName: RestExceptionHandler
 * @Description: TODO
 * @author starhq
 * @createtime 2014年9月7日 下午8:55:09
 * @email : jian.wang@shinsoft.net
 * @modify by TODO
 * @resaon
 * @edittime
 * @version 1.0
 * @copyright shinsoft
 */
public class RestExceptionHandler extends AbstractHandlerExceptionResolver
		implements InitializingBean {

	private static final Logger LOGGER = LoggerFactory
			.getLogger(RestExceptionHandler.class);

	private HttpMessageConverter<?>[] messageConverters = null;

	private List<HttpMessageConverter<?>> allMessageConverters = null;

	private RestErrorResolver errorResolver;

	private RestErrorConverter<?> errorConverter;

	private ContentNegotiationManager contentNegotiationManager;

	public void setContentNegotiationManager(
			ContentNegotiationManager contentNegotiationManager) {
		this.contentNegotiationManager = contentNegotiationManager;
	}

	public RestExceptionHandler() {
		this.errorResolver = new DefaultRestErrorResolver();
		this.errorConverter = new MapRestErrorConverter();
	}

	public void setMessageConverters(HttpMessageConverter<?>[] messageConverters) {
		this.messageConverters = messageConverters;
	}

	public void setErrorResolver(RestErrorResolver errorResolver) {
		this.errorResolver = errorResolver;
	}

	public RestErrorResolver getErrorResolver() {
		return this.errorResolver;
	}

	public RestErrorConverter<?> getErrorConverter() {
		return errorConverter;
	}

	public void setErrorConverter(RestErrorConverter<?> errorConverter) {
		this.errorConverter = errorConverter;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
	 */
	@Override
	public void afterPropertiesSet() throws Exception {
		ensureMessageConverters();
	}

	@SuppressWarnings("unchecked")
	private void ensureMessageConverters() {

		List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();

		// user configured values take precedence:
		if (this.messageConverters != null && this.messageConverters.length > 0) {
			converters.addAll(CollectionUtils
					.arrayToList(this.messageConverters));
		}

		// defaults next:
		new HttpMessageConverterHelper().addDefaults(converters);

		this.allMessageConverters = converters;
	}

	// leverage Spring's existing default setup behavior:
	private static final class HttpMessageConverterHelper extends
			WebMvcConfigurationSupport {
		public void addDefaults(List<HttpMessageConverter<?>> converters) {
			addDefaultHttpMessageConverters(converters);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver
	 * #doResolveException(javax.servlet.http.HttpServletRequest,
	 * javax.servlet.http.HttpServletResponse, java.lang.Object,
	 * java.lang.Exception)
	 */
	@Override
	protected ModelAndView doResolveException(HttpServletRequest request,
			HttpServletResponse response, Object handler, Exception ex) {

		ServletWebRequest webRequest = new ServletWebRequest(request, response);

		RestErrorResolver resolver = getErrorResolver();

		RestError error = resolver.resolveError(webRequest, handler, ex);
		if (error == null) {
			return null;
		}

		ModelAndView mav = null;

		try {
			mav = getModelAndView(webRequest, handler, error);
		} catch (Exception invocationEx) {
			LOGGER.error("Acquiring ModelAndView for Exception [" + ex
					+ "] resulted in an exception.", invocationEx);
		}

		return mav;
	}

	protected ModelAndView getModelAndView(ServletWebRequest webRequest,
			Object handler, RestError error) throws Exception {

		applyStatusIfPossible(webRequest, error);

		Object body = error; // default the error instance in case they don't
								// configure an error converter

		RestErrorConverter converter = getErrorConverter();
		if (converter != null) {
			body = converter.convert(error);
		}

		return handleResponseBody(body, webRequest);
	}

	private void applyStatusIfPossible(ServletWebRequest webRequest,
			RestError error) {
		if (!WebUtils.isIncludeRequest(webRequest.getRequest())) {
			webRequest.getResponse().setStatus(error.getStatus().value());
		}
		// TODO support response.sendError ?
	}

	@SuppressWarnings("unchecked")
	private ModelAndView handleResponseBody(Object body,
			ServletWebRequest webRequest) throws ServletException, IOException {

//		HttpInputMessage inputMessage = new ServletServerHttpRequest(
//				webRequest.getRequest());

		// List<MediaType> acceptedMediaTypes = inputMessage.getHeaders()
		// .getAccept();

		List<MediaType> acceptedMediaTypes = getMediaTypes(webRequest
				.getRequest());

		if (acceptedMediaTypes.isEmpty()) {
			acceptedMediaTypes = Collections.singletonList(MediaType.ALL);
		}

		MediaType.sortByQualityValue(acceptedMediaTypes);

		HttpOutputMessage outputMessage = new ServletServerHttpResponse(
				webRequest.getResponse());

		Class<?> bodyType = body.getClass();

		List<HttpMessageConverter<?>> converters = this.allMessageConverters;

		if (converters != null) {
			for (MediaType acceptedMediaType : acceptedMediaTypes) {
				for (HttpMessageConverter messageConverter : converters) {
					if (messageConverter.canWrite(bodyType, acceptedMediaType)) {
						messageConverter.write(body, acceptedMediaType,
								outputMessage);
						// return empty model and view to short circuit the
						// iteration and to let
						// Spring know that we've rendered the view ourselves:
						return new ModelAndView();
					}
				}
			}
		}

		if (logger.isWarnEnabled()) {
			logger.warn("Could not find HttpMessageConverter that supports return type ["
					+ bodyType + "] and " + acceptedMediaTypes);
		}
		return null;
	}

	protected List<MediaType> getMediaTypes(HttpServletRequest request) {
		try {
			ServletWebRequest webRequest = new ServletWebRequest(request);

			List<MediaType> acceptableMediaTypes = this.contentNegotiationManager
					.resolveMediaTypes(webRequest);
			acceptableMediaTypes = acceptableMediaTypes.isEmpty() ? Collections
					.singletonList(MediaType.ALL) : acceptableMediaTypes;

			List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request);
			Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
			for (MediaType acceptable : acceptableMediaTypes) {
				for (MediaType producible : producibleMediaTypes) {
					if (acceptable.isCompatibleWith(producible)) {
						compatibleMediaTypes.add(getMostSpecificMediaType(
								acceptable, producible));
					}
				}
			}
			List<MediaType> selectedMediaTypes = new ArrayList<MediaType>(
					compatibleMediaTypes);
			MediaType.sortBySpecificityAndQuality(selectedMediaTypes);
			if (logger.isDebugEnabled()) {
				logger.debug("Requested media types are " + selectedMediaTypes
						+ " based on Accept header types "
						+ "and producible media types " + producibleMediaTypes
						+ ")");
			}
			return selectedMediaTypes;
		} catch (HttpMediaTypeNotAcceptableException ex) {
			return null;
		}
	}

	@SuppressWarnings("unchecked")
	private List<MediaType> getProducibleMediaTypes(HttpServletRequest request) {
		Set<MediaType> mediaTypes = (Set<MediaType>) request
				.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
		if (!CollectionUtils.isEmpty(mediaTypes)) {
			return new ArrayList<MediaType>(mediaTypes);
		} else {
			return Collections.singletonList(MediaType.ALL);
		}
	}

	private MediaType getMostSpecificMediaType(MediaType acceptType,
			MediaType produceType) {
		produceType = produceType.copyQualityValue(acceptType);
		return MediaType.SPECIFICITY_COMPARATOR
				.compare(acceptType, produceType) < 0 ? acceptType
				: produceType;
	}

}
